﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tomahawk.Runtime.Rendering.Materials;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Tomahawk.Runtime.Rendering.Objects;
using Tomahawk.Runtime.Utils;
using Tomahawk.Runtime.Rendering.Particles;
using Tomahawk.Runtime.Rendering.LightningBolts;
using Tomahawk.Runtime.Scene;

namespace Tomahawk.Runtime.Rendering
{
    public sealed class SASRenderEnvironment
    {
        #region Transformations
        public Matrix world;
        public Matrix view;
        public Matrix projection;
        public float time = 0;
        public float elapsedTime = 0;
        #endregion

        #region Textures
        public Texture DiffuseMap = null;
        public Texture NormalMap = null;
        public Texture SpecularMap = null;
        public Texture EnvironmentMap = null;
        public Texture LightMap = null;
        #endregion

        #region Illumination
        public struct LightDefinition
        {
            public Vector4 position;
            public Vector4 direction;
            public Vector4 diffuse;
            public Vector4 specular;
            public Vector4 ambient;
        }

        public LightDefinition light0;
        public LightDefinition light1;
        public LightDefinition light2;
        #endregion

        #region RenderStateCache
        internal struct RenderStateCache
        {
            internal VertexBuffer vertexBuffer;
            internal IndexBuffer indexBuffer;
            internal VertexDeclaration vertexDeclaration;
            internal int streamOffset;

            internal struct MatrixCache
            {
                internal bool dirty;
                internal Matrix matrix;
            }

            internal MatrixCache worldView;
            internal MatrixCache viewProjection;
            internal MatrixCache worldViewProjection;
            internal MatrixCache viewInverse;
            internal MatrixCache worldInverse;
            internal MatrixCache worldViewInverse;
            internal MatrixCache worldInverseTranspose;
            internal MatrixCache worldViewInverseTranspose;

            internal void ResetBuffers()
            {
                vertexBuffer = null;
                indexBuffer = null;
                vertexDeclaration = null;
                streamOffset = -1;
            }

            internal void ResetPerFrameMatrices()
            {
                viewProjection.dirty = true;
                viewInverse.dirty = true;
            }

            internal void ResetPerObjectMatrices()
            {
                worldView.dirty = true;
                worldViewProjection.dirty = true;
                worldInverse.dirty = true;
                worldViewInverse.dirty = true;
                worldInverseTranspose.dirty = true;
                worldViewInverseTranspose.dirty = true;
            }

            internal void ResetAll()
            {
                ResetBuffers();
                ResetPerFrameMatrices();
                ResetPerObjectMatrices();
            }
        }

        internal RenderStateCache cache;
        #endregion
    }

    #region RenderCommands
    internal sealed class RenderCommand
    {
        #region Events
        public delegate void ExecuteCommandDelegate(SASMaterial material);
        public event ExecuteCommandDelegate OnExecuteCommand;

        private void RaiseOnExecuteCommand(SASMaterial material)
        {
            if (OnExecuteCommand != null) OnExecuteCommand(material);
        }
        #endregion

        internal Matrix world = Matrix.Identity;
        internal SASMaterial material = null;
        internal VertexBuffer vertexBuffer = null;
        internal IndexBuffer indexBuffer = null;
        internal VertexDeclaration vertexDeclaration = null;
        internal int streamOffset = 0;
        internal SASScript.Context context = SASScript.Context.EmptyContext;

        internal void Clear()
        {
            material = null;
            vertexBuffer = null;
            indexBuffer = null;
            vertexDeclaration = null;
            OnExecuteCommand = null;
            context = SASScript.Context.EmptyContext;
        }

        internal bool Execute(SASRenderEnvironment environment, GraphicsDevice device)
        {
            SASEffect sasEffect = material != null ? material.Effect : null;
            if (sasEffect == null || sasEffect.CurrentTechnique == null)
            {
                return false;
            }

            if (indexBuffer != null && indexBuffer.IsDisposed)
            {
                return false;
            }

            if (vertexBuffer != null && vertexBuffer.IsDisposed)
            {
                return false;
            }

            if (vertexDeclaration != null && vertexDeclaration.IsDisposed)
            {
                return false;
            }

            bool result = false;

            RaiseOnExecuteCommand(material);

            if (indexBuffer != environment.cache.indexBuffer)
            {
                device.Indices = indexBuffer;
                environment.cache.indexBuffer = indexBuffer;
            }

            if (vertexDeclaration != environment.cache.vertexDeclaration)
            {
                device.VertexDeclaration = vertexDeclaration;
                environment.cache.vertexDeclaration = vertexDeclaration;
            }

            if (vertexBuffer != environment.cache.vertexBuffer || streamOffset != environment.cache.streamOffset)
            {
                device.Vertices[0].SetSource(
                    vertexBuffer,
                    streamOffset,
                    vertexDeclaration.GetVertexStrideSize(0));

                environment.cache.vertexBuffer = vertexBuffer;
                environment.cache.streamOffset = streamOffset;
            }

            context.sasEnvironment = Engine.Instance.RenderManager.Environment;
            context.sasEffect = sasEffect;
            context.sasTechnique = sasEffect.CurrentTechnique;
            context.device = device;

            result = sasEffect.CurrentTechnique.DrawMesh(ref context);

            return true;
        }
    }
    #endregion

    #region RenderManager
    public class RenderManager
    {
        #region Profiling
        static Profile RenderSceneSection = Profile.Get("RenderManager.RenderScene");
        static Profile RenderCanvasSection = Profile.Get("RenderManager.RenderCanvas");
        static Profile ExecuteCommandsSection = Profile.Get("RenderManager.ExecuteRenderCommands");
        #endregion

        public enum Layers
        {
            Background_1,   // For 2D games
            Background_2,   // For 2D games
            Background_3,   // For 2D games

            Standard,       // For 3D games
            Transparency,   // For 3D games

            Gameplay_1,     // For 2D games
            Gameplay_2,     // For 2D games
            Gameplay_3,     // For 2D games

            Foreground_1,   // For 2D games
            Foreground_2,   // For 2D games
            Foreground_3,   // For 2D games

            LayerCount,     // Must be the last layer
        };

        internal class RenderNodeList : List<SceneNode> { public RenderNodeList(int size) : base(size) { } }
        internal class CommandQueue : Queue<RenderCommand> { public CommandQueue(int size) : base(size) { } }
        internal class CommandQueueTable : Dictionary<SASEffect, CommandQueue> { }

        #region Delegates
        public delegate SASEffect.RenderGroup GenerateRenderGroupIdDelegate(string name);
        public GenerateRenderGroupIdDelegate GenerateRenderGroupId = null;
        #endregion

        #region Properties
        internal RenderNodeList[] renderNodes = null;
        internal CommandQueueTable[] renderCommands;

        protected SASRenderEnvironment environment = new SASRenderEnvironment();
        public SASRenderEnvironment Environment { get { return environment; } }

        private CommandQueue renderCommandPool = new CommandQueue(1000);
        private Queue<CommandQueue> commandQueuePool = new Queue<CommandQueue>(100);

        private SASMaterial objectShaderOverride = null;
        public SASMaterial ObjectShaderOverride { get { return objectShaderOverride; } set { objectShaderOverride = value; } }

        private SASMaterial sceneShader = null;
        public SASMaterial SceneShader
        {
            get { return this.sceneShader; }
            set
            {
                if (this.sceneShader != null)
                {
                    this.sceneShader.Effect.UnloadSurfaces(Engine.Instance.GraphicsDevice);
                }

                this.sceneShader = value;
                this.sceneShader.Effect.LoadSurfaces(Engine.Instance.GraphicsDevice);
                this.sceneShader.Effect.OnDrawGeometry += new SASSceneEffect.DrawGeometryDelegate(OnDrawGeometry);
                this.sceneShader.Effect.OnDrawCanvas += new SASSceneEffect.DrawCanvasDelegate(OnDrawCanvas);
            }
        }
        #endregion

        public RenderManager()
        {
            int layerCount = (int)Layers.LayerCount;
            renderCommands = new CommandQueueTable[layerCount];
            for (int index = 0; index < layerCount; ++index)
            {
                renderCommands[index] = new CommandQueueTable();
            }

            int renderGroupCount = (int)SASEffect.RenderGroup.RenderGroupCount;
            renderNodes = new RenderNodeList[renderGroupCount];
            for (int index = 0; index < renderGroupCount; ++index)
            {
                renderNodes[index] = new RenderNodeList(1000);
            }
        }

        #region Command Pools
        internal RenderCommand BuildRenderCommand(ModelMesh mesh, ModelMeshPart part, ref Matrix worldMatrix, SASMaterial material)
        {
            RenderCommand command = null;
            if (renderCommandPool.Count <= 0)
            {
                command = new RenderCommand();
            }
            else
            {
                command = renderCommandPool.Dequeue();
            }

            command.indexBuffer = mesh.IndexBuffer;
            command.vertexBuffer = mesh.VertexBuffer;
            command.vertexDeclaration = part.VertexDeclaration;
            command.streamOffset = part.StreamOffset;

            command.context.primitiveType = PrimitiveType.TriangleList;
            command.context.baseVertex = part != null ? part.BaseVertex : 0;
            command.context.numVertices = part != null ? part.NumVertices : 0;
            command.context.indexedVertices = true;
            command.context.startIndex = part != null ? part.StartIndex : 0;
            command.context.primitiveCount = part != null ? part.PrimitiveCount : 0;

            command.world = worldMatrix;
            command.material = material;

            return command;
        }

        internal RenderCommand BuildRenderCommand(MaterialQuad quad, ref Matrix worldMatrix, SASMaterial material)
        {
            RenderCommand command = null;
            if (renderCommandPool.Count <= 0)
            {
                command = new RenderCommand();
            }
            else
            {
                command = renderCommandPool.Dequeue();
            }

            command.indexBuffer = quad.Indices;
            command.vertexBuffer = quad.Vertices;
            command.vertexDeclaration = quad.Declaration;
            command.streamOffset = 0;

            command.context.primitiveType = PrimitiveType.TriangleList;
            command.context.baseVertex = 0;
            command.context.numVertices = 4;
            command.context.indexedVertices = true;
            command.context.startIndex = 0;
            command.context.primitiveCount = 2;

            command.world = worldMatrix;
            command.material = material;

            return command;
        }

        internal RenderCommand BuildRenderCommand(ParticleSystem particles, int firstParticle, int particleCount, ref Matrix worldMatrix, SASParticles material)
        {
            RenderCommand command = null;
            if (renderCommandPool.Count <= 0)
            {
                command = new RenderCommand();
            }
            else
            {
                command = renderCommandPool.Dequeue();
            }

            command.indexBuffer = null;
            command.vertexBuffer = particles.VertexBuffer;
            command.vertexDeclaration = particles.VertexDeclaration;
            command.streamOffset = 0;

            command.context.primitiveType = PrimitiveType.PointList;
            command.context.baseVertex = firstParticle;
            command.context.numVertices = 1;
            command.context.indexedVertices = false;
            command.context.startIndex = 0;
            command.context.primitiveCount = particleCount;

            command.world = worldMatrix;
            command.material = material;

            return command;
        }

        internal RenderCommand BuildRenderCommand(LightningBolt bolt, int vertexCount, int primitiveCount, ref Matrix worldMatrix, SASLightningBolt material)
        {
            RenderCommand command = null;
            if (renderCommandPool.Count <= 0)
            {
                command = new RenderCommand();
            }
            else
            {
                command = renderCommandPool.Dequeue();
            }

            command.indexBuffer = bolt.IndexBuffer;
            command.vertexBuffer = bolt.VertexBuffer;
            command.vertexDeclaration = bolt.VertexDeclaration;
            command.streamOffset = 0;

            command.context.primitiveType = PrimitiveType.TriangleList;
            command.context.baseVertex = 0;
            command.context.numVertices = vertexCount;
            command.context.indexedVertices = true;
            command.context.startIndex = 0;
            command.context.primitiveCount = primitiveCount;

            command.world = worldMatrix;
            command.material = material;

            return command;
        }

        public void ClearRenderCommandPool()
        {
            renderCommandPool.Clear();
        }
        #endregion

        #region Rendering
        internal bool ReloadSurfaces(GraphicsDevice device)
        {
            bool result = false;
            if (sceneShader != null && sceneShader.Effect != null)
            {
                result = sceneShader.Effect.LoadSurfaces(device);
            }

            return result;
        }

        protected bool OnDrawGeometry(string script, SASEffect.RenderGroup group, GraphicsDevice device)
        {
            #region Begin Profiling Section
#if PROFILER
            using (IDisposable d = RenderSceneSection.Measure())
            {
#endif
            #endregion

                if (group != SASEffect.RenderGroup.None)
                {
                    RenderNodeList nodes = renderNodes[(int)group];
                    for (int node = 0; node < nodes.Count(); ++node)
                    {
                        nodes[node].Render(device, false);
                    }
                }

                #region End Profiling Section
#if PROFILER
            }
#endif
                #endregion

            #region Begin Profiling Section
#if PROFILER
            using (IDisposable d = ExecuteCommandsSection.Measure())
            {
#endif
            #endregion
                ExecuteCommands(device);
                #region End Profiling Section
#if PROFILER
            }
#endif
                #endregion

            return true;
        }

        protected bool OnDrawCanvas(string script, GraphicsDevice device)
        {
            #region Begin Profiling Section
#if PROFILER
            using (IDisposable d = RenderCanvasSection.Measure())
            {
#endif
            #endregion
                Engine.Instance.CanvasManager.Render();
                #region End Profiling Section
#if PROFILER
            }
#endif
                #endregion

            return true;
        }

        internal bool AddCommand(RenderCommand command, Layers layer)
        {
            CommandQueue queue = null;
            CommandQueueTable table = renderCommands[(int)layer];

            if (objectShaderOverride != null)
            {
                if (!table.TryGetValue(objectShaderOverride.Effect, out queue))
                {
                    if (commandQueuePool.Count <= 0)
                    {
                        queue = new CommandQueue(1000);
                    }
                    else
                    {
                        queue = commandQueuePool.Dequeue();
                    }

                    table.Add(objectShaderOverride.Effect, queue);
                }

                command.material = objectShaderOverride;
                queue.Enqueue(command);
            }
            else
            {
                if (!table.TryGetValue(command.material.Effect, out queue))
                {
                    if (commandQueuePool.Count <= 0)
                    {
                        queue = new CommandQueue(1000);
                    }
                    else
                    {
                        queue = commandQueuePool.Dequeue();
                    }

                    table.Add(command.material.Effect, queue);
                }

                queue.Enqueue(command);
            }

            return true;
        }

        private bool OnVisitScene(SceneNode node)
        {
            int renderGroupCount = (int)SASEffect.RenderGroup.RenderGroupCount;
            for (int renderGroup = 0; renderGroup <= renderGroupCount; ++renderGroup)
            {
                int mask = 1 << renderGroup;
                if (node.Visible && (((int)node.RenderGroups) & mask) != 0)
                {
                    renderNodes[renderGroup].Add(node);
                }
            }

            return true;
        }

        internal bool PreRender(GraphicsDevice device)
        {
            Engine.Instance.SceneManager.Visit(OnVisitScene, SceneManager.VisitSceneFlags.AllRenderableNodes);
            return true;
        }

        internal bool PostRender(GraphicsDevice device)
        {
            int renderGroupCount = (int)SASEffect.RenderGroup.RenderGroupCount;
            for (int renderGroup = 0; renderGroup < renderGroupCount; ++renderGroup)
            {
                renderNodes[renderGroup].Clear();
            }
            return true;
        }

        protected bool ExecuteCommands(GraphicsDevice device)
        {
            bool result = true;

            environment.cache.ResetAll();

            int renderCommandsCount = renderCommands.Count();
            for (int tableId = 0; tableId < renderCommandsCount; ++tableId)
            {
                Layers layer = (Layers)tableId;
                CommandQueueTable table = renderCommands[tableId];

                foreach (KeyValuePair<SASEffect, CommandQueue> queue in table)
                {
                    SASEffect effect = queue.Key;
                    effect.Update(environment, SASParameter.UpdateType.UpdatePerFrame);

                    effect.BeginCommands(device);

                    while (queue.Value.Count > 0)
                    {
                        RenderCommand command = queue.Value.Dequeue();

                        environment.cache.ResetPerObjectMatrices();

                        environment.DiffuseMap = command.material.DiffuseMap;
                        environment.NormalMap = command.material.NormalMap;
                        environment.SpecularMap = command.material.SpecularMap;
                        environment.EnvironmentMap = command.material.EnvironmentMap;
                        environment.LightMap = command.material.LightMap;
                        environment.world = command.world;

                        effect.Update(environment, SASParameter.UpdateType.UpdatePerObject);
                        result &= command.Execute(environment, device);
                        command.Clear();

                        renderCommandPool.Enqueue(command);
                    }

                    queue.Value.Clear();
                    commandQueuePool.Enqueue(queue.Value);

                    effect.EndCommands(device);
                }

                table.Clear();
            }

            environment.cache.ResetBuffers();

            return result;
        }

        internal bool ClearCommands()
        {
            for (int layerId = 0; layerId < renderCommands.Count(); ++layerId)
            {
                CommandQueueTable table = renderCommands[layerId];
                foreach (KeyValuePair<SASEffect, CommandQueue> queue in table)
                {
                    queue.Value.Clear();
                }
            }

            renderCommandPool.Clear();
            commandQueuePool.Clear();

            return true;
        }

        #endregion
    }
    #endregion
}
